threading macros from dash for Emacs Lisp | Yoo Box
目录
1. what are threading macros? where do they come from?
* 1。什么是线程宏?他们来自哪里?
属性: :CUSTOM_ID: sec-1 :CUSTOM_ID sec-1
结束:
For example, what is this 例如,这是什么
(-> 2 (expt 0.2) (* 2017) sin) ; ⇒ -1.0
The Clojure language has some neat macros (such as ->
and ->>
) which are called threading macros (nothing to do with these threads) which let you unwrap some types of deeply nested forms. The dash library for Emacs Lisp implements some of those threading macros. While I do not use threading macros in my elisp code, I know that elisp beginners would love using these macros, so I wrote this article.
Clojure语言有一些简洁的宏(如=->=和=->>=),它们被称为/threading macros/(与这些线程无关),可以打开某些类型的深度嵌套表单。Emacs Lisp的dash库实现了一些线程化宏。虽然我在我的elisp代码中没有使用线程宏,但我知道elisp初学者会喜欢使用这些宏,所以我写了这篇文章。
Prerequisites: Readers are not required to be familiar with Clojure or dash, but you are assumed to be able to install the dash library. Unwillingness to check the length of the word humuhumunukunukuapuaa by hand is required. 先决条件:读者/不/不需要/熟悉Clojure或dash,但是假定您能够安装dash库。不愿意用手检查单词humuhumunukunukuapuaa的长度是需要的。
Common Lisp Note: These threading macros can be defined in Common Lisp too. When you define them, don't forget that threading macros are more than just function call chaining. What I mean by that? I'll get to that soon. 注意:这些线程宏也可以在Common Lisp中定义。在定义它们时,不要忘记线程宏不仅仅是函数调用链接。我的意思是?我很快就会讲到。
2. how to use the dash library
* 2。如何使用dash库
属性: :CUSTOM_ID: sec-2 :CUSTOM_ID sec-2
结束:
After you install the dash library, you will have to put (require 'dash)
somewhere in your init file. Where?
在安装了dash库之后,必须将=(require 'dash)=放在init文件中的某个位置。在哪里?
You probably have something like the following two lines in your init file. 您的init文件中可能有以下两行代码。
(package-initialize) (setq package-enable-at-startup nil)
You want to make sure that the line (require 'dash)
runs after (package-initialize)
runs, but before any code that relies on functions or macros from the dash library runs. Simplest way to do that is to put (require 'dash)
right after the package-initialize lines so that your init file code looks like:
您需要确保line =(require 'dash)=在=(package-initialize)=运行之后,但在任何依赖于dash库中的函数或宏的代码运行之前运行。最简单的方法是put =(require 'dash)=后面的包初始化行,这样你的init文件代码看起来就像:
... (package-initialize) (setq package-enable-at-startup nil) (require 'dash) ...
3. thread-first nesting and the thread-first macro
* 3。线程优先嵌套和线程优先宏
属性: :CUSTOM_ID: sec-3 :CUSTOM_ID sec-3
结束:
Before we begin, let's talk about deeply nested forms. 在开始之前,让我们讨论一下深度嵌套的表单。
Let's start with: 让我们开始:
(f3 (f2 (f1 x c1 d1) c2 d2) c3 d3)
That is an f1 form within an f2 form within an f3 form. If f1, f2, f3 were functions (as opposed to macros), that could mean “take object x, apply f1 to it with additional arguments c1 and d1, then apply f2 to the result with additional arguments c2 and c2, then apply f3 to the result ...”. 这是一个f1的形式,一个f2的形式,一个f3的形式。如果f1, f2, f3是函数(相对于宏),这可能意味着“取对象x,用附加参数c1和d1对它应用f1,然后用附加参数c2和c2对结果应用f2,然后对结果应用f3……”。
To help make your code more readable, you could write that form in multiple lines as: 为了让你的代码更具可读性,你可以把表单写成多行:
(f3 (f2 (f1 x c1 d1) c2 d2) c3 d3)
or: 或者:
(f3 (f2 (f1 x c1 d1) c2 d2) c3 d3)
or in combination of both styles depending on which arguments are complex forms themselves. 或者结合这两种样式,这取决于哪些参数本身是复杂的形式。
Alternatively, you can use the macro ->
(the “thread-first” macro from the dash library) to write this instead:
或者,你可以使用宏=->=(“线程优先”宏从dash库)来写这个:
(-> x (f1 c1 d1) (f2 c2 d2) (f3 c3 d3))
4. example uses of the thread-first macro
* 4。示例使用线程优先宏
属性: :CUSTOM_ID: sec-4 :CUSTOM_ID sec-4
结束:
There is a saying,”never date anyone under half your age plus seven”. Suppose you are a 200 year old turtle. You are not supposed to date turtles under age 107. You take the number 200, divide it by 2, then add 7, that's 107. You can compute that with this form which you must read inside-out: 有句话说,“永远不要和比你小一半的人约会,再加上7岁”。假设你是一只200岁的乌龟。你不应该和107岁以下的乌龟约会。200除以2,然后加7,等于107。你可以计算这个表格,你必须从内到外阅读:
(+ (/ 200 2) 7)
You can also write the same computation using the ->
macro like this which you can read from left to right rather than inside-out:
你也可以用=->=宏写同样的计算,就像这样,你可以从左到右读,而不是由内而外:
(-> 200 (/ 2) (+ 7))
Some argue that writing an inside-out expression is unnatural for humans, but I heard from somewhere that the English expression “Sum the balance of all savings accounts” is a perfectly natural inside-out expression (inside-out from a procedural perspective). The threading macros (and serial binding forms that I will get to) give you choice: you can either write an inside-out expression or an expression to be read from left to right (or from top to bottom). 有些人认为写一个由内而外的表达式对人类来说是不自然的,但我从某个地方听说过英语表达“合计所有储蓄账户的余额”是一个完全自然的由内而外的表达式(从程序的角度来看是由内而外的)。线程化宏(以及我将介绍的串行绑定形式)为您提供了选择:您可以编写一个由内而外的表达式,也可以编写一个从左到右(或从上到下)读取的表达式。
You've seen an example of a ->
form that you read left to right. Now let's see an example that you read from top to bottom. The following code starts with a long list, then removes duplicates from the list, then removes 0s and 1s, and then sorts it.
您已经看到了一个从左到右读取的=->=表单示例。现在让我们来看一个你从上到下阅读的例子。下面的代码从一个长列表开始,然后从列表中删除重复项,然后删除0和1,然后排序。
(-> (list 9 9 9 1 0 1 0 3 3) (cl-remove-duplicates) (cl-set-difference (list 0 1)) (sort '<)) ;; ⇒ (3 9)
You could take the length of the final list instead like this: 你可以取最终列表的长度,像这样:
(-> (list 9 9 9 1 0 1 0 3 3) (cl-remove-duplicates) (cl-set-difference (list 0 1)) (length)) ;; ⇒ 2
That in turn can be written simpler like this: 反过来可以写得更简单,就像这样:
(-> (list 9 9 9 1 0 1 0 3 3) cl-remove-duplicates (cl-set-difference (list 0 1)) length) ; <-- instead of (length)
5. side note on fear of deeply nested forms
* 5。附注:对深度嵌套表单的恐惧
属性: :CUSTOM_ID: sec-5 :CUSTOM_ID sec-5
结束:
Some Lisp beginners tend to fear reading and writing of deeply nested forms (even three or four levels of nesting could feel too deep). Since this article tend to attract those beginners, I'd like to include my explanation for why you should not fear. 一些Lisp初学者倾向于害怕读和写深度嵌套的表单(即使是三到四层嵌套也会觉得太深)。因为这篇文章倾向于吸引那些初学者,所以我想解释一下为什么你不应该害怕。
For reading deeply nested forms, sometimes keybindings for structural movement (for example, C-M-u
) help a lot when reading from indentation seems not enough. For writing, with paredit you will be able to figure out a way to write a nested form from inside out, or from outside in, or whatever order you choose to write. With these tips in mind, one can eventually overcome fear of something like:
对于读取深度嵌套的表单,有时用于结构移动的键绑定(例如=C-M-u=)在从缩进中读取数据时帮助很大。对于编写,使用paredit,您将能够找到一种方法来编写一个嵌套的表单,从内到外,或从外到内,或您选择的任何顺序。有了这些提示,一个人最终可以克服对某些事情的恐惧,比如:
;; from color.el (defun color-saturate-name (name percent) "Make a color with a specified NAME more saturated by PERCENT." (apply 'color-rgb-to-hex (apply 'color-hsl-to-rgb (apply 'color-saturate-hsl (append (apply 'color-rgb-to-hsl (color-name-to-rgb name)) (list percent))))))
Maybe read my previous articles on how to read Lisp code easily and how to edit Lisp code easily. End of side note. 请阅读我之前的文章如何轻松阅读Lisp代码和如何轻松编辑Lisp代码。边注结束。
6. thread-last nesting and the thread-last macro
* 6。线程最后的嵌套和线程最后的宏
属性: :CUSTOM_ID: sec-6 :CUSTOM_ID sec-6
结束:
(f3 a3 b3 (f2 a2 b2 (f1 a1 b1 x)))
can be written in multiline as: 可以用多行写成:
(f3 a3 b3 (f2 a2 b2 (f1 a1 b1 x)))
or as: 或者为:
(f3 a3 b3 (f2 a2 b2 (f1 a1 b1 x)))
or you can use the macro ->>
(the “thread-last” macro from the dash library) to write that instead as:
或者你可以使用宏=->>= (" thread-last "宏从短跑库)来代替写:
(->> x (f1 a1 b1) (f2 a2 b2) (f3 a3 b3))
7. example uses of the thread-last macro
* 7。示例使用了thread-last宏
属性: :CUSTOM_ID: sec-7 :CUSTOM_ID sec-7
结束:
(->> "1 3 5 7 9 11 13 15 17 19" split-string (mapcar 'string-to-int) (cl-reduce '+)) ;; ⇒ 100
That splits the string to get a list of strings, then maps string-to-int
to the list in order to get a list of numbers, then sums the numbers.
它将字符串分割成一个字符串列表,然后将=string-to-int=映射到该列表以获得一个数字列表,然后对这些数字求和。
8. thread-middle macro
* 8。thread-middle宏
属性: :CUSTOM_ID: sec-8 :CUSTOM_ID sec-8
结束:
This deeply nested Lisp form 这个深度嵌套的Lisp表单
(f3 a3 b3 (f2 a2 b2 (f1 a1 b1 x c1 d1) c2 d2) c3 d3)
can be indented like 可以缩进吗
(f3 a3 b3 (f2 a2 b2 (f1 a1 b1 x c1 d1) c2 d2) c3 d3)
or like 或者像
(f3 a3 b3 (f2 a2 b2 (f1 a1 b1 x c1 d1) c2 d2) c3 d3)
That can be written using the macro -->
as:
可以使用宏=——>= as:
(--> x (f1 a1 b1 it c1 d1) (f2 a2 b2 it c2 d2) (f3 a3 b3 it c3 d3))
Clojure Note: Clojure users who want to use thread-middle macro in Clojure code should see Generalized Threading Macro in Clojure. 注意:希望在Clojure代码中使用线程中间宏的Clojure用户应该查看[[http://stackoverflow.com/questions/10068398/generalizing-threading-macro -in- Clojure] [Clojure中的通用线程宏]]。
9. rewriting some deeply nested form as a serial binding
* 9。将深度嵌套的表单重写为串行绑定
属性: :CUSTOM_ID: sec-9 :CUSTOM_ID sec-9
结束:
If f1, f2, f3 are functions (as opposed to macros), one can also simply write this: 如果f1, f2, f3是函数(相对于宏),我们也可以简单地这样写:
(let ((it x)) (setq it (f1 a1 b1 it c1 d1) it (f2 a2 b2 it c2 d2)) (f3 a3 b3 it c3 d3))
or this: 或:
(let* ((it x) (it (f1 a1 b1 it c1 d1)) (it (f2 a2 b2 it c2 d2))) (f3 a3 b3 it c3 d3))
or you can use the threading macro. 或者你可以使用线程宏。
10. threading macros are more than serial binding
* 10。线程宏不仅仅是串行绑定
属性: :CUSTOM_ID: sec-10 :CUSTOM_ID sec-10
结束:
Threading macros can be more than just chaining function calls because you can use them with other macros like loop macros or conditionals. For example, you can write your own REPL (Read Eval Print Loop) like this: 线程化宏不仅仅是链接函数调用,因为您可以将它们与其他宏(如循环宏或条件)一起使用。例如,你可以这样写你自己的REPL(读Eval打印循环):
(-> (read t) ; Read eval ; Eval print ; Print (cl-loop (sit-for 1))) ; Loop
which expands to: 扩大到:
(cl-loop (print (eval (read t))) (sit-for 1))
(Try it. You can get out of the infinite loop by pressing C-g
)
(试一试。可以通过按=C-g=)跳出无限循环
Is humuhumunukunukuapuaa a long word? I would consider words longer than 20 letters as long words. humuhumunukunukuapuaa是一个很长的单词吗?我认为超过20个字母的单词就是长单词。
(--> "humuhumunukunukuapuaa" (length it) (< it 20) (if it 'short 'long)) ;; ⇒ long
Yes, it is long. 是的,它很长。
11. closing notes
* 11。关闭笔记
属性: :CUSTOM_ID: sec-11 :CUSTOM_ID sec-11
结束:
- This article is part of the Living with Emacs Lisp series.
-这篇文章是与Emacs Lisp一起生活系列的一部分。
- Why are they called threading macros? I do not know.
-为什么它们被称为线程宏?我不知道。
Everything I want beginners to know for this topic is covered now. The rest is optional reading. 我想让初学者知道的关于这个主题的所有内容现在都有了。其余的是选读。
12. optional reading
* 12。可选的阅读
属性: :CUSTOM_ID: sec-12 :CUSTOM_ID sec-12
结束:
12.1. sum under reciprocal
12.1 * * *。在互惠的总和
属性: :CUSTOM_ID: sec-12-1 :CUSTOM_ID sec-12-1
结束:
Alice takes 30 minutes to finish a bowl of jjajangmyeon. Bob takes 40 minutes to finish the same. With Alice and Bob working together on the same one bowl of jjajangmyeon, how many minutes does it take to finish the bowl? Sum of 30 minutes and 40 minutes under reciprocal. To calculate it, 爱丽丝要花30分钟才能喝完一碗jjajangmyeon。鲍勃花了40分钟完成同样的工作。爱丽丝和鲍勃一起在一碗jjajangmyeon上工作,完成一碗需要多少分钟?30分钟和40分钟的总和互惠。来计算,
(->> (list 30 40) (--map (/ 1.0 it)) (-reduce '+) (/ 1.0)) ;; ⇒ 17.142857142857142
So it takes about 17 minutes. 大概需要17分钟。
12.2. art of minimizing use of thread-middle macro
12.2 * * *。最小化线程中间宏使用的艺术
属性: :CUSTOM_ID: sec-12-2 :CUSTOM_ID sec-12-2
结束:
In Clojure, consensus seems to be that Clojure libraries should be designed in such a way that users usually only have to use just one of the thread-first macro and the thread-last macro just once for a group of steps. The dash library and the s library are two Emacs Lisp libraries that sticks to that Clojure consensus and that is a sort of selling point of the two libraries. For example, many functions from dash that work on lists consistently take the list as the last argument so that you can use just the thread-last macro with them. If you want to get the most out of threading macros, you may want to start depending on functions from the two libraries. 在Clojure中,人们一致认为Clojure库的设计方式应该是,对于一组步骤,用户通常只需使用一个线程优先宏和一个线程最后宏中的一个。dash库和s库是两个坚持Clojure共识的Emacs Lisp库,这是这两个库的某种卖点。例如,许多在列表上工作的dash函数始终将列表作为最后一个参数,这样您就可以对它们使用thread-last宏。如果您想充分利用线程化宏,您可能需要开始依赖这两个库中的函数。
My examples in this article show some reliance on CL-LIB functions (rather than functions from the two libraries: dash and s) because I tend to depend on CL-LIB functions and also because I am not assuming the readers to be familiar with functions from the two libraries. (I tend to use CL-LIB more because it's shipped with Emacs.) 我在本文中展示一些例子依赖[[http://www.gnu.org/software/emacs/manual/html_node/cl/] [CL-LIB]]函数(而不是从两个库函数:破折号和s)因为我倾向于依赖CL-LIB功能也因为我不假设读者熟悉两个库的功能。(我更倾向于使用CL-LIB,因为它是Emacs附带的。)
Clojure programmers sometimes come to a situation where they have to write a form that seems to require two or three times last-argument threading and just one first-argument or middle-argument threading. In that case, some of them tend to use a neat trick to manage to write it with the thread-last macro (rather than write it with the thread-middle macro). An Emacs Lisp equivalent would be, for example, you might be using the s library and you want to take a list of strings, trim them, then join them with comma, and then wrap the result in curly braces using just the threading-last macro, but you are wondering what to do with the last step. You can just do this: Clojure程序员有时会遇到这样的情况,他们必须编写一个表单,该表单似乎需要两到三次后参数线程化,而只需一次前参数或中间参数线程化。在这种情况下,他们中的一些人倾向于使用一个巧妙的技巧来设法用线程最后的宏(而不是用线程中间的宏)来编写它。一个Emacs Lisp等价的,例如,你可能使用图书馆,你想要一个字符串列表,修剪它们,然后用逗号,加入他们的行列,然后用花括号中的结果使用threading-last宏,但是你不知道如何处理的最后一步。你可以这样做:
(require 's) (->> (list " bacon " "milk" "tofu") (-map 's-trim) (s-join ", ") ((lambda (s) (concat "{" s "}"))))
That's the trick (the use of lambda in the last step). Actually in this particular case, you don't need that trick, you can just write: 这就是诀窍(在最后一步中使用lambda)。实际上在这种情况下,你不需要这个技巧,你可以这样写:
(->> (list " bacon " "milk" "tofu") (-map 's-trim) (s-join ", ") (s-prepend "{") (s-append "}"))
Or you can use this trick too. 或者你也可以使用这个技巧。